﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using epk23.PartIIProject.Exceptions;
using epk23.PartIIProject;
using Tomers.WPF.Shapes;
using System.Windows.Controls;
using System.Windows.Shapes;
using System.Windows.Input;
using System.Windows;
using System.Windows.Media;

namespace epk23.PartIIProject
{
    class MachineGraph
    {
        public Machine machine { get; set; }
        public static Canvas canvas { get; set; }
        public static MachineWindow window { get; set; }
        private List<RuleNode> nodes;
        public List<RuleNode> Nodes
        {
            get { return nodes; }
        }

        private List<GraphArrow> arrows;
        public List<GraphArrow> Arrows
        {
            get { return arrows; }
        }

        private Ellipse nodeMarker;

        private List<bool> ruleNumbers; //boolean list representing available rule numbers (true == available)
        public List<bool> RuleNumbers
        {
            get { return ruleNumbers; }
        }

        private bool movingArrow;
        private GraphArrow clickedArrow;

        public bool draggingNode;
        public bool settingStart;

        private bool changingRegister;
        private string newRegName;

        private ConcreteNode markedNode;

        private ConcreteNode dragNode;
        private Type dragNodeType;
        private Point dragOldPosition;
        private Point dragStartPosition;

        private UIAction currentAction;
        private Stack<UIAction> undoStack;
        private Stack<UIAction> redoStack;

        public MachineGraph()
        {
            machine = new Machine();
            nodes = new List<RuleNode>();
            ruleNumbers = new List<bool>();
            arrows = new List<GraphArrow>();
            undoStack = new Stack<UIAction>();
            redoStack = new Stack<UIAction>();

            settingStart = false;
            movingArrow = false;

            makeNodeMarker();
            nodeMarker.Opacity = 0;
        }

        public void makeNodeMarker()
        {
            nodeMarker = new Ellipse();
            nodeMarker.Opacity = 0;
            canvas.Children.Add(nodeMarker);
            nodeMarker.Stroke = System.Windows.Media.Brushes.Red;
            nodeMarker.Fill = System.Windows.Media.Brushes.Red;
            nodeMarker.Width = 10;
            nodeMarker.Height = 10;
            nodeMarker.SetValue(Canvas.LeftProperty, 0.0);
            nodeMarker.SetValue(Canvas.TopProperty, 0.0);
            nodeMarker.SetValue(Canvas.ZIndexProperty, Int32.MaxValue);
            nodeMarker.IsHitTestVisible = true;
        }

        private RuleNode getStartNode()
        {
            RuleNode startNode = null;

            foreach (ConcreteNode node in nodes)
            {
                if (node.getRule().ruleNumber == machine.startRuleNumber)
                {
                    startNode = (RuleNode)node;
                }
            }

            return startNode;
        }

        public void resetNodeMarker()
        {
            RuleNode startNode = getStartNode();
            markedNode = (ConcreteNode)startNode;
            setNodeMarker(startNode);
        }

        public void setMarkedNode(ConcreteNode node)
        {
            markedNode = node;
            setNodeMarker((RuleNode)markedNode);
        }

        private void setNodeMarker(RuleNode node)
        {
            nodeMarker.SetValue(Canvas.LeftProperty, node.x);
            nodeMarker.SetValue(Canvas.TopProperty, node.y);
        }

        public void addIncrRuleNode(double x, double y, string regName, Canvas canvas)
        {
            GraphState oldState = this.getState();

            nodeMarker.Opacity = 1.0;

            IncrRuleNode newNode = new IncrRuleNode(x, y, regName, canvas);

            newNode.addToCanvas();

            makeDefaultArrows(newNode);

            if (machine.rules.Count == 0)
                setMarkedNode(newNode);

            machine.addRule(newNode.getRule());

            GraphState newState = this.getState();

            currentAction = new ChangeGraphAction(this, oldState, newState);
            undoStack.Push(currentAction);
        }

        public void addDecrRuleNode(double x, double y, string regName, Canvas canvas)
        {
            GraphState oldState = this.getState();

            nodeMarker.Opacity = 1.0;

            DecrRuleNode newNode = new DecrRuleNode(x, y, regName, canvas);

            newNode.addToCanvas();

            makeDefaultArrows(newNode);

            if (machine.rules.Count == 0)
                setMarkedNode(newNode);

            resetNodeMarker();

            machine.addRule(newNode.getRule());

            GraphState newState = this.getState();

            currentAction = new ChangeGraphAction(this, oldState, newState);
            undoStack.Push(currentAction);
        }

        public void addHaltRuleNode(double x, double y, Canvas canvas)
        {
            GraphState oldState = this.getState();

            nodeMarker.Opacity = 1.0;

            HaltRuleNode newNode = new HaltRuleNode(x, y, canvas);

            newNode.addToCanvas();

            if (machine.rules.Count == 0)
                setMarkedNode(newNode);

            machine.addRule(newNode.getRule());

            GraphState newState = this.getState();

            currentAction = new ChangeGraphAction(this, oldState, newState);
            undoStack.Push(currentAction);
        }

        public void addMachineNode(double x, double y, string machineName, Canvas canvas)
        {
            GraphState oldState = this.getState();

            nodeMarker.Opacity = 1.0;

            string description = FileManager.readFile(machineName).Machine.description;

            MachineNode newNode = new MachineNode(x, y, machineName, description, canvas);

            newNode.addToCanvas();

            if (machine.rules.Count == 0)
                setMarkedNode(newNode);

            makeDefaultArrows(newNode);

            foreach (String s in FileManager.readFile(machineName).Machine.getRegisters())
                machine.addRegister(s);

            machine.addRule(newNode.getRule());

            GraphState newState = this.getState();

            currentAction = new ChangeGraphAction(this, oldState, newState);
            undoStack.Push(currentAction);
        }

        public void addNode(RuleNode n)
        {
            nodes.Add(n);
        }

        public void addArrow(GraphArrow gArrow)
        {
            arrows.Add(gArrow);
        }

        public void deleteNode(ConcreteNode nodeToDelete)
        {
            GraphState oldState = this.getState();

            nodes.Remove((RuleNode)nodeToDelete);
            nodeToDelete.delete();
            Rule ruleToDelete = nodeToDelete.getRule();
            machine.removeRule(ruleToDelete);
            ruleNumbers[ruleToDelete.ruleNumber] = true;

            if (nodeToDelete.getRule().ruleNumber == machine.startRuleNumber)
            {
                if (machine.rules.Count == 0)
                {
                    nodeMarker.Opacity = 0.0;
                    machine.startRuleNumber = 0;
                }
                else
                {
                    machine.startRuleNumber = machine.rules[0].ruleNumber;
                    resetNodeMarker();
                }

            }

            GraphState newState = this.getState();
            currentAction = new ChangeGraphAction(this, oldState, newState);
            undoStack.Push(currentAction);
        }

        public int getNextRuleNumber()
        {
            bool found = false;
            int n = -1;
            int i = 0;
            foreach (bool b in ruleNumbers)
            {
                if (b == true)
                {
                    n = i;
                    found = true;
                    break;
                }

                i++;
            }
            if (found)
                ruleNumbers[n] = false;
            else
            {
                ruleNumbers.Add(false);
                n = ruleNumbers.Count - 1;
            }

            if (n < 0)
                throw new InvalidRuleNumberException();

            return n;
        }

        public ConcreteNode lookupNode(int ruleNumber)
        {
            ConcreteNode returnNode = null;

            foreach (ConcreteNode node in nodes)
            {
                if (node.getRule().ruleNumber == ruleNumber)
                {
                    returnNode = node;
                    break;
                }
            }
            if (returnNode == null)
                throw new NodeNotFoundException();

            return returnNode;
        }

        public void makeDefaultArrows(ConcreteNode node)
        {
            Arrow newArrow = makeNewArrow();

            GraphArrow newGraphArrow = new GraphArrow((RuleNode)node, (RuleNode)node, newArrow, 0);
            addArrow(newGraphArrow);

            if (node.GetType() == typeof(DecrRuleNode))
            {
                Arrow newArrow2 = makeNewArrow();

                newArrow2.StrokeDashArray = Values.ArrowStrokeDashArray;

                GraphArrow newGraphArrow2 = new GraphArrow((RuleNode)node, (RuleNode)node, newArrow2, 1);
                addArrow(newGraphArrow2);
            }
        }

        private Arrow makeNewArrow()
        {
            Arrow newArrow = new Arrow();

            newArrow.MouseDown += arrowMouseDown;
            newArrow.MouseEnter += arrowMouseEnter;
            newArrow.MouseLeave += arrowMouseLeave;

            newArrow.Stroke = System.Windows.Media.Brushes.Black;
            newArrow.StrokeThickness = 5;
            newArrow.HeadWidth = 7;
            newArrow.HeadHeight = 7;
            newArrow.SetValue(Canvas.ZIndexProperty, 10);
            canvas.Children.Add(newArrow);

            return newArrow;
        }

        public void setArrows(RuleNode node)
        {
            foreach (GraphArrow arrow in arrows)
            {
                if (arrow.startNode == node)
                {
                    arrow.setEndNode(null);
                }
            }
        }

        public void changeRegister(string newRegName)
        {
            changingRegister = true;
            this.newRegName = newRegName;
        }

        public void arrowMouseDown(object sender, MouseButtonEventArgs e)
        {
            GraphState oldState = this.getState();
            currentAction = new ChangeGraphAction(this, oldState, null);

            movingArrow = true;

            foreach (GraphArrow graphArrow in arrows)
            {
                if (graphArrow.arrow == sender)
                    clickedArrow = graphArrow;
            }

            ((Arrow)sender).Stroke = new SolidColorBrush(System.Windows.Media.Colors.Green);
        }

        public void arrowMouseEnter(object sender, MouseEventArgs e)
        {
            if (!movingArrow)
                ((Arrow)sender).Stroke = new SolidColorBrush(System.Windows.Media.Colors.Red);
        }

        public void arrowMouseLeave(object sender, MouseEventArgs e)
        {
            if (!movingArrow)
                ((Arrow)sender).Stroke = new SolidColorBrush(System.Windows.Media.Colors.Black);
        }

        public void nodeBodyMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (!movingArrow && !settingStart && !changingRegister)
            {
                dragNode = this.getNode(sender);
                dragNodeType = dragNode.GetType();
                dragOldPosition = e.GetPosition(canvas);
                dragStartPosition = new Point(((RuleNode)dragNode).x, ((RuleNode)dragNode).y);
                draggingNode = true;

                ((RuleNode)dragNode).nodeBody.IsHitTestVisible = false;
            }
        }

        public void nodeBodyMouseUp(object sender, MouseButtonEventArgs e)
        {
            if (changingRegister)
            {
                changingRegister = false;

                GraphState oldState = this.getState();

                ConcreteNode nodeToChange = getNode(sender);
                Type nodeType = nodeToChange.GetType();
                if (nodeType == typeof(IncrRuleNode) || nodeType == typeof(DecrRuleNode))
                {

                    string oldRegName = nodeToChange.getRule().targetRegister;

                    nodeToChange.getRule().targetRegister = newRegName;
                    machine.addRegister(newRegName);

                    if (nodeType == typeof(IncrRuleNode))
                        ((RuleNode)nodeToChange).label.Content = newRegName + "+";
                    else if (nodeType == typeof(DecrRuleNode))
                        ((RuleNode)nodeToChange).label.Content = newRegName + "-";

                    bool found = false;
                    foreach (Rule rule in machine.rules)
                    {
                        if (rule.targetRegister == oldRegName)
                            found = true;
                    }
                    if (!found)
                        machine.registers.Remove(machine.lookupRegister(oldRegName));

                    GraphState newState = this.getState();

                    undoStack.Push(new ChangeGraphAction(this,oldState,newState));

                    window.refreshRegValues();
                }
            }

            if (settingStart)
            {
                settingStart = false;

                RuleNode oldStartNode = null;
                foreach (ConcreteNode node in nodes)
                {
                    if (node.getRule().ruleNumber == machine.startRuleNumber)
                        oldStartNode = (RuleNode)node;
                }

                RuleNode newStartNode = (RuleNode)getNode(sender);
                setStartNode(newStartNode);

                undoStack.Push(new SetStartNodeAction(oldStartNode, newStartNode, this));
            }

            if (movingArrow)
            {
                movingArrow = false;
                RuleNode oldEndNode = clickedArrow.endNode;
                RuleNode newEndNode = (RuleNode)getNode(sender);
                moveArrow(clickedArrow, newEndNode);

                foreach (GraphArrow gArrow in arrows)
                {
                    if (gArrow.startNode == newEndNode)
                        gArrow.setPosition();
                }

                clickedArrow.arrow.Stroke = new SolidColorBrush(System.Windows.Media.Colors.Black);

                GraphState newState = this.getState();
                ((ChangeGraphAction)currentAction).newState = newState;
                undoStack.Push(currentAction);
            }
        }

        public void nodeBodyMouseMove(object sender, MouseEventArgs e)
        {
            
        }

        public void canvasMouseMove(MouseEventArgs e)
        {
            if (draggingNode)
            {
                Point currentPosition = e.GetPosition(canvas);
                double cx = (double)((RuleNode)dragNode).x;
                double cy = (double)((RuleNode)dragNode).y;

                double newX = cx + currentPosition.X - dragOldPosition.X;
                double newY = cy + currentPosition.Y - dragOldPosition.Y;

                if (cx < 0)
                {
                    dragNode.setPosition(0, newY);
                    endDrag();
                }
                else if (cy < 0)
                {
                    dragNode.setPosition(newX, 0);
                    endDrag();
                }
                else if (cx + ((RuleNode)dragNode).nodeBody.Width > canvas.Width)
                {
                    moveNode(dragNode, canvas.Width - ((RuleNode)dragNode).nodeBody.Width, newY);
                    endDrag();
                }
                else if (cy + ((RuleNode)dragNode).nodeBody.Height > canvas.Height)
                {
                    moveNode(dragNode, newX, canvas.Height - ((RuleNode)dragNode).nodeBody.Height);
                    endDrag();
                }
                else
                {
                    moveNode(dragNode, newX, newY);
                    dragOldPosition = currentPosition;
                }
            }
        }

        public void canvasMouseUp(MouseButtonEventArgs e)
        {
                endDrag();
        }

        public void endDrag()
        {
            if (draggingNode)
            {
                draggingNode = false;
                ((RuleNode)dragNode).nodeBody.IsHitTestVisible = true;
                ((RuleNode)dragNode).fillBrush.Color = Values.NodeColour;
                RuleNode draggedNode = (RuleNode)dragNode;
                undoStack.Push(new MoveNodeAction(dragNode, dragStartPosition, new Point(draggedNode.x, draggedNode.y), this));
            }
        }

        public void nodeBodyMouseEnter(object sender, MouseEventArgs e)
        {
            RuleNode mousedNode = (RuleNode)this.getNode(sender);
            mousedNode.deleteLabel.Visibility = System.Windows.Visibility.Visible;

            mousedNode.fillBrush.Color = Values.MousedNodeColour;
            mousedNode.fillBrush.Opacity = 1;

            if (mousedNode.GetType() == typeof(MachineNode))
            {
                window.showDescription(((MachineNode)mousedNode).label.Content+": "+((MachineNode)mousedNode).description);
            }
        }

        public void nodeBodyMouseLeave(object sender, MouseEventArgs e)
        {
            Point mousePosition = e.GetPosition(canvas);
            RuleNode mousedNode = (RuleNode)this.getNode(sender);
            Label deleteLabel = mousedNode.deleteLabel;
            double labelX = Canvas.GetLeft(deleteLabel);
            double labelY = Canvas.GetTop(deleteLabel);
            int labelSize = Values.DeleteLabelSize;

            if (!(mousePosition.X > labelX && mousePosition.X < labelX + labelSize && mousePosition.Y > labelY && mousePosition.Y < labelY + labelSize))
                ((RuleNode)this.getNode(sender)).deleteLabel.Visibility = System.Windows.Visibility.Hidden;

            if (!draggingNode || dragNode != mousedNode)
            {
                mousedNode.fillBrush.Color = Values.NodeColour;
                mousedNode.fillBrush.Opacity = 0.6;
            }
        }

        public void deleteLabelMouseUp(object sender, MouseButtonEventArgs e)
        {
            ConcreteNode nodeToDelete = ((ConcreteNode)this.getNode(sender));
            deleteNode(nodeToDelete);
        }

        public void deleteLabelMouseEnter(object sender, MouseEventArgs e)
        {
            RuleNode mousedNode = (RuleNode)this.getNode(sender);
            mousedNode.fillBrush.Color = System.Windows.Media.Colors.Pink;
        }

        public void deleteLabelMouseLeave(object sender, MouseEventArgs e)
        {
            RuleNode mousedNode = (RuleNode)this.getNode(sender);
            if (mousedNode != null)
            {
                mousedNode.fillBrush.Color = System.Windows.Media.Colors.White;
            }
        }

        public void setStartNode(RuleNode newStartNode)
        {
            int newStartRuleNumber = ((ConcreteNode)newStartNode).getRule().ruleNumber;
            machine.startRuleNumber = newStartRuleNumber;

            setMarkedNode((ConcreteNode)newStartNode);
        }

        private void moveArrow(GraphArrow movedArrow, RuleNode newEndNode)
        {

            clickedArrow.setEndNode(newEndNode);

            if (movedArrow.startNode.GetType() == typeof(IncrRuleNode))
            {
                ((IncrRuleNode)movedArrow.startNode).rule.targetRuleNumber = ((ConcreteNode)newEndNode).getRule().ruleNumber;
            }
            else if (movedArrow.startNode.GetType() == typeof(MachineNode))
            {
                ((MachineNode)movedArrow.startNode).rule.targetRuleNumber = ((ConcreteNode)newEndNode).getRule().ruleNumber;
            }
            else if (movedArrow.startNode.GetType() == typeof(DecrRuleNode))
            {
                if (movedArrow.type == 0)
                {
                    ((DecrRuleNode)movedArrow.startNode).rule.targetRuleNumber = ((ConcreteNode)newEndNode).getRule().ruleNumber;
                }
                else if (movedArrow.type == 1)
                {
                    ((DecrRuleNode)movedArrow.startNode).rule.targetRuleNumber2 = ((ConcreteNode)newEndNode).getRule().ruleNumber;
                }
            }
        }

        public void moveNode(ConcreteNode node, double x, double y)
        {
            node.setPosition(x, y);
            setArrows();
            if (node == markedNode)
                setNodeMarker((RuleNode)node);
        }

        private void setArrows()
        {
            foreach (GraphArrow arrow in arrows)
            {
                arrow.setPosition();
            }
        }

        public static void loadGraph(MachineWithLayout layout, MachineGraph graph)
        {

            graph.nodes.Clear();
            graph.arrows.Clear();
            graph.ruleNumbers = new List<bool>();


            Machine m = layout.Machine;
            List<int[]> layoutList = layout.Layout;

            graph.machine = m;

            foreach (Rule r in m.rules)
            {
                double x = layout.getPosition(r.ruleNumber).X;
                double y = layout.getPosition(r.ruleNumber).Y;


                if (r.GetType() == typeof(IncrementRule))
                {
                    IncrRuleNode newNode = new IncrRuleNode(x, y, r.targetRegister, canvas);
                    newNode.rule = (IncrementRule)r;
                    newNode.getRule().ruleNumber = r.ruleNumber;
                    newNode.addToCanvas();
                }

                else if (r.GetType() == typeof(DecrementRule))
                {
                    DecrRuleNode newNode = new DecrRuleNode(x, y, r.targetRegister, canvas);
                    newNode.rule = (DecrementRule)r;
                    newNode.getRule().ruleNumber = r.ruleNumber;
                    newNode.addToCanvas();
                }
                else if (r.GetType() == typeof(HaltRule))
                {
                    HaltRuleNode newNode = new HaltRuleNode(x, y, canvas);
                    newNode.rule = (HaltRule)r;
                    newNode.getRule().ruleNumber = r.ruleNumber;
                    newNode.addToCanvas();
                }
                else if (r.GetType() == typeof(MachineRule))
                {
                    MachineNode newNode = new MachineNode(x, y, ((MachineRule)r).machineName, FileManager.readFile(((MachineRule)r).machineName).Machine.description, canvas);
                    newNode.rule = (MachineRule)r;
                    newNode.getRule().ruleNumber = r.ruleNumber;
                    newNode.addToCanvas();
                }

            }

            List<String> regNames = m.getRegisters();

            foreach (String s in regNames)
                m.addRegister(s);

            foreach (Rule r in m.rules)
            {
                if (r.GetType() == typeof(IncrementRule))
                {
                    RuleNode startNode = (RuleNode)graph.lookupNode(r.ruleNumber);
                    RuleNode endNode = (RuleNode)graph.lookupNode(((IncrementRule)r).targetRuleNumber);
                    Arrow newArrow = graph.makeNewArrow();
                    graph.addArrow(new GraphArrow(startNode, endNode, newArrow, 0));
                }
                else if (r.GetType() == typeof(MachineRule))
                {
                    RuleNode startNode = (RuleNode)graph.lookupNode(r.ruleNumber);
                    RuleNode endNode = (RuleNode)graph.lookupNode(((MachineRule)r).targetRuleNumber);
                    Arrow newArrow = graph.makeNewArrow();
                    graph.addArrow(new GraphArrow(startNode, endNode, newArrow, 0));
                }
                else if (r.GetType() == typeof(DecrementRule))
                {
                    RuleNode startNode = (RuleNode)graph.lookupNode(r.ruleNumber);
                    RuleNode endNode1 = (RuleNode)graph.lookupNode(((DecrementRule)r).targetRuleNumber);
                    RuleNode endNode2 = (RuleNode)graph.lookupNode(((DecrementRule)r).targetRuleNumber2);
                    Arrow newArrow1 = graph.makeNewArrow();
                    Arrow newArrow2 = graph.makeNewArrow();
                    newArrow2.StrokeDashArray = Values.ArrowStrokeDashArray;
                    graph.addArrow(new GraphArrow(startNode, endNode1, newArrow1, 0));
                    graph.addArrow(new GraphArrow(startNode, endNode2, newArrow2, 1));
                }

            }
        }

        private GraphState getState()
        {
            GraphState state = new GraphState();

            Machine m = machine;

            foreach (Rule r in m.rules)
            {
                String ruleString = FileManager.ruleToString(r);
                state.rules.Add(ruleString);
            }

            foreach (RuleNode node in Nodes)
            {
                int[] coord = new int[3];
                coord[0] = ((ConcreteNode)node).getRule().ruleNumber;
                coord[1] = (int)node.x;
                coord[2] = (int)node.y;

                state.coords.Add(coord);
            }

            state.startRuleNum = m.startRuleNumber;

            return state;
        }

        public void loadState(GraphState state)
        {
            Machine m = new Machine();

            foreach (String ruleString in state.rules)
                m.addRule(FileManager.readRule(ruleString));

            m.startRuleNumber = state.startRuleNum;

            canvas.Children.Clear();

            loadGraph(new MachineWithLayout(m, state.coords), this);

            makeNodeMarker();

            if (nodes.Count > 0)
            {
                RuleNode start = (RuleNode)lookupNode(machine.startRuleNumber);
                resetNodeMarker();
            }
        }

        public void clearGraph()
        {
            GraphState oldState = this.getState();

            nodes.Clear();
            arrows.Clear();
            machine = new Machine();
            ruleNumbers = new List<bool>();

            GraphState newState = this.getState();

            undoStack.Push(new ChangeGraphAction(this, oldState, newState));

        }

        private ConcreteNode getNode(Object o)
        {
            ConcreteNode clickedNode = null;
            foreach (RuleNode r in nodes)
            {
                if (r.nodeBody == o || r.deleteLabel == o)
                    clickedNode = (ConcreteNode)r;
            }
            return clickedNode;
        }

        public void zeroRegisters()
        {
            foreach (Register r in machine.registers)
                r.value = 0;

            window.refreshRegValues();
        }

        public void undo()
        {
            if (undoStack.Count > 0)
            {
                UIAction action = undoStack.Pop();
                action.undo();
                redoStack.Push(action);
            }
        }

        public void redo()
        {
            if (redoStack.Count > 0)
            {
                UIAction action = redoStack.Pop();
                action.redo();
                undoStack.Push(action);
            }
        }
    }
}

